<?php
/**
 * sonar报告主程序
 * User: wzd
 * Date: 2019/10/28
 * Time: 18:27
 */
use com\rainiswalking\sonar_report\lib\HttpUtil;
use com\rainiswalking\sonar_report\lib\CommonUtil;

__app_init();

// 是否适用内置demo数据进行运行，用于开发测试适用。实际使用请设为false
define('RUN_WITH_MOCK_DATA', false);

$cfg = Spyc::YAMLLoad(ROOT_PATH.'sonar.yml');
$report = new SonarReport($cfg);
$report->report();


//------------------------------------------------------------------------------------------------
function __app_init() {
    ini_set('display_errors',0);
    error_reporting(E_ALL & ~(E_STRICT|E_NOTICE|E_WARNING));
    define('ROOT_PATH', dirname(__FILE__).'/');
    spl_autoload_register(function ($class) {
        if (strpos($class, "\\") === FALSE) { // 避免非命名空间写法进入此逻辑，防止误调用
            return ;
        }
        $prefix = "com\\rainiswalking\\sonar_report";
        if (strpos($class, $prefix) === 0) {
            $class = substr($class, strlen($prefix)+1);
            $file = ROOT_PATH.$class.'.php';
            $file = str_replace("\\", "/", $file);
            if (file_exists($file)) {
                include ($file);
            }
        }
    });
    // venders目录标准命名空间 类自动加载
    spl_autoload_register(function($className) {
        $basePath = ROOT_PATH . 'venders/';
        $classFile = $basePath . str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php';
        if (function_exists('stream_resolve_include_path')) {
            $file = stream_resolve_include_path($classFile);
        } else {
            $file = false;
            foreach (explode(PATH_SEPARATOR, get_include_path()) as $path) {
                if (file_exists($path . '/' . $classFile)) {
                    $file = $path . '/' . $classFile;
                    break;
                }
            }
        }
        /* If file is found, store it into the cache, classname <-> file association */
        if (($file !== false) && ($file !== null)) {
            include $file;
            return;
        }
    });

    set_error_handler(array($this,'log_php_error_handler'),
        E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR |E_USER_ERROR |E_RECOVERABLE_ERROR );
    set_exception_handler(array($this,'log_php_exception_handler'));

    function log_php_error_handler($errno , $errstr , $errfile , $errline){
        if($errno==E_USER_NOTICE){		//module trigger error
            CommonUtil::log_error('USER'.lang('req_log_info') ."[{$errno}] : {$errstr},in file {$errfile}[{$errline}]");
            return true;
        }else if($errno==E_ERROR || $errno== E_USER_ERROR || $errno==E_CORE_ERROR || $errno==E_COMPILE_ERROR) {
            CommonUtil::log_error('PHP '.lang('req_log_error') ."[{$errno}] : {$errstr},in file {$errfile}[{$errline}]");
        }else if($errno == E_WARNING || $errno == E_NOTICE) {
            CommonUtil::log_warn('PHP '.lang('req_log_warn') ."[{$errno}] : {$errstr},in file {$errfile}[{$errline}]");
        }else if($errno != E_STRICT) {
            CommonUtil::log_debug('PHP '.lang('req_log_debug') ."[{$errno}] : {$errstr},in file {$errfile}[{$errline}]");
        } else {
            CommonUtil::log_error('PHP '.lang('req_log_error') ."[{$errno}] : {$errstr},in file {$errfile}[{$errline}]");
        }
    }

    function log_php_exception_handler($e){
        CommonUtil::log_error('PHP '.lang('req_log_except'). "[{$e->getCode()}] : {$e->getMessage()},in file {$e->getFile()}[{$e->getLine()}]");
    }
}
class SonarReport {
    const REPORT_BY_PROJECT = 'project';
    const REPORT_BY_AUTHOR = 'author';

    const LOGFILE = 'sonar-report.log';

    function __construct($cfg) {
        CommonUtil::assert_field_required($cfg['sonar'], 'url');
        CommonUtil::assert_field_required($cfg['sonar'], 'user_token');
        CommonUtil::assert_field_required($cfg['sonar'], 'report_by');

        CommonUtil::assert_field_required($cfg['notice'], 'type');

        if (substr($cfg['sonar']['url'], -1) == '/') {
            $cfg['sonar']['url'] = substr($cfg['sonar']['url'], 0, -1);
        }

        $this->cfg = $cfg;
    }

    function _get_projects() {
        if (RUN_WITH_MOCK_DATA) {
            return array(
                'components'=>array(
                    array('key'=>'A'),
                    array('key'=>'B'),
                )
            );
        }
        $ret = $this->request('api/components/search_projects', array('filter'=>$this->cfg['sonar']['project_filter']));

        return $ret;
    }

    function get_issues($keys) {
        // 默认BUG和漏洞
        $types = CommonUtil::get_field_value($this->cfg['sonar'], 'issue_types', 'BUG,VULNERABILITY');

        // 默认获取打开状态
        $statuses = CommonUtil::get_field_value($this->cfg['sonar'], 'issue_statuses', 'OPEN,REOPENED');

        CommonUtil::log_info("get_issues: type={$types} | statuses={$statuses}");

        // FIXME 暂时通过获取问题再统计数量的方式，不适合问题较多的情况，先按最多500处理
        $args = array(
            'statuses'=>$statuses,
            'types'=>$types,
            'componentKeys'=>$keys,
            'ps'=>1,
            'facets'=>'projects,authors,types'
        );
        $list = $this->request('api/issues/search', $args);

        return $list;
    }

    function project_statistc($report_by, $projects) {
        if (RUN_WITH_MOCK_DATA) {
            return array(
                'A'=>array('type'=>'A', 'count'=>10),
                'B'=>array('type'=>'B', 'count'=>2)
            );
        }
        $keys = CommonUtil::implode($projects, 'key', ',');
        // 获取符合条件的问题
        $_list = $this->get_issues($keys);

        // 通过接口提供的统计能力来计算问题数量
        $stat_values = array();
        foreach ($_list['facets'] as $_facet) {
            if ($_facet['property'] == $report_by.'s') {
                $stat_values = $_facet['values'];
                break;
            }
        }

        $projects_map = CommonUtil::build_map($projects, 'key');
        $count_arr = array();
        foreach ($stat_values as $row) {
            // FIXME 过滤掉不在指定项目里的数据
            if ($report_by == self::REPORT_BY_PROJECT && !isset($projects_map[$row['val']])) {
                continue;
            }
            $count_arr[$row['val']] = array('type'=>$row['val'], 'count'=>$row['count']);
        }

        return $count_arr;
    }

    /**
     * 可以按项目或作者的维度来报告
     */
    function report() {
        $report_by = CommonUtil::get_field_value($this->cfg['sonar'], 'report_by', 'project');
        $notice_by = CommonUtil::get_field_value($this->cfg['notice'], 'type', '');
        // 查询需要统计的项目
        $ret_projects = $this->_get_projects();
        if (empty($ret_projects) || empty($ret_projects['components'])) {
            CommonUtil::log_info("no projects found");
            return ;
        }

        $count_arr = $this->project_statistc($report_by, $ret_projects['components']);

        $data = $this->genera_report($report_by, $ret_projects['components'], $count_arr);
        if (empty($data['content'])) {
            CommonUtil::log_info("notice content is empty. exit");
            return ;
        }


        //  根据配置获取通知的方式，并执行通知
        $notice_types = explode(',', $notice_by);
        foreach ($notice_types as $type) {
            $type = trim($type);
            $clazz = 'com\\rainiswalking\\sonar_report\\notice_plugins\\'.ucfirst($type).'NoticePlugin';
            $obj = new $clazz;
            CommonUtil::log_info("notice by {$type}: ".json_encode($data));
            $is_success = $obj->notice($data, $this->cfg);
            $msg = $is_success ? 'success' : 'fail';
            CommonUtil::log_info("notice by ".$type.' '.$msg);
        }

    }

    /**
     * 生成报告的模板
     * @param $report_by
     * @param $projects 项目列表
     * @param $count_arr
     * 形如array(
     *  array('type'=>'', 'count'=>1)
     * )
     */
    function genera_report($report_by, $projects, $count_arr) {
        $count_arr = CommonUtil::array_sort($count_arr, 'count', SORT_DESC);

        $str = '';
        $total_error_count = 0;
        $error_projects_count = 0;
        foreach ($count_arr as $_row) {
            $total_error_count += $_row['count'];
            if ($_row['count'] > 0) {
                $str .=  '- '.sprintf('%-30s', $_row['type'])."\t".$_row['count']."\r\n";
                $error_projects_count++;
            }
        }

        $tips = $total_error_count > 20 ? '尽快修复！' : '';

        if ($report_by ==  self::REPORT_BY_PROJECT) {
            $ok_arr = array();
            foreach ($projects as $row) {
                if (!isset($count_arr[$row['key']])) {
                    $ok_arr[] = $row['key'];
                }
            }
            if (count($ok_arr) > 0) {
                $str = count($ok_arr)."个仓库（".implode($ok_arr, ',')."）没有问题\r\n".$str;
            }

            $title = '共'.$total_error_count.'个问题。'. $error_projects_count.'个仓库有问题，'.$tips;
        } else if ($report_by ==  self::REPORT_BY_AUTHOR) {
            $title = $error_projects_count.'开发者存在代码漏洞，共'.$total_error_count.'个。'.$tips;
        }

        return array('title'=>$title, 'content'=>$str);
    }


    /** 请求sonar接口
     * @param $method
     * @param $params
     * @return array 数据格式换回
     */
    private function request($method, $params) {
        $opts = array(
            'header'=>array(
                "Authorization: Basic ".base64_encode($this->cfg['sonar']['user_token'])
            )
        );

        $ret = HttpUtil::post($this->cfg['sonar']['url'].'/'.$method, $params, $opts);
        return json_decode($ret, true);
    }
}